package cave.renderer;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import cave.game.MainActivity;
import cave.utils.IntArray;
import cave.utils.OrderedArray;

import android.util.Log;

import com.badlogic.gdx.backends.android.AndroidGL20;
import com.badlogic.gdx.backends.android.surfaceview.GLSurfaceView;
import com.badlogic.gdx.graphics.GL20;

public class Renderer implements GLSurfaceView.Renderer {

	private static AndroidGL20 GLES20 = new AndroidGL20();
	
	public static final int MSG_CONTEXTCREATED = 0;
	public static final int MSG_RENDERFPS = 1;
	
	private final int UNI_VIEWPOSSCALE = 0;	
	private final int UNI_POSSCALE = 1;
	private final int UNI_UVPOSSCALE = 2;
	private final int UNI_ROT = 3;
	private final int UNI_COLOR = 4;
	private final int UNI_TEX = 5;
	
	private final int ATTR_POS = 0;
	private final int ATTR_UV = 1;
	
	private boolean firstCreate = true;
	private volatile boolean validResources = true;
	private volatile boolean sendContextCreateMsg = false;
	
	private float aspectRatio;
	
	private RenderList renderList;
	private RenderMsgQueue renderMsgQueue;
	private IntArray outIntData;
	
	private int vertShaderId, fragShaderId, programId;
	private int[] programUniforms, programAttributes;
	private float[][] viewPosScale = {new float[4], new float[4]};
	
	private int vboId;

	private OrderedArray<Integer> textures;
	
	// FPS CODE
	private long fpsStart;
	private int frames;
	
	public Renderer() {
		renderList = new RenderList();
		renderMsgQueue = new RenderMsgQueue();
		outIntData = new IntArray();
		textures = new OrderedArray<Integer>();
				
	}
	
	@Override
	public void onDrawFrame(GL10 gl) {
		synchronized (this) {
			
			processMessages(renderMsgQueue);
			renderMsgQueue.clear();
			
			GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
			GLES20.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

			if (validResources) {
			
				GLES20.glBindBuffer(GL20.GL_ARRAY_BUFFER, vboId);
				
				GLES20.glEnableVertexAttribArray(programAttributes[ATTR_POS]);			
				GLES20.glVertexAttribPointer(programAttributes[ATTR_POS], 2, GL20.GL_FLOAT, false, 4*4, 0);
				
				GLES20.glEnableVertexAttribArray(programAttributes[ATTR_UV]);
				GLES20.glVertexAttribPointer(programAttributes[ATTR_UV], 2, GL20.GL_FLOAT, false, 4*4, 2*4);
				
				GLES20.glUseProgram(programId);
				GLES20.glUniform1i(programUniforms[UNI_TEX], 0);
	
				int boundTexture = 0;
				
				int dataIndex = 0;
				float[] spriteData = renderList.getSprites().getInternalArray();
				IntArray listInfo = renderList.getListInfo();
				for (int i = 0; i < listInfo.size(); ++i) {
					int type = listInfo.get(i) >>> 16;
					int count = listInfo.get(i) & 0x0000FFFF;
					if (count > 0) {
						
						// Set transformation uniforms
						float[] view = viewPosScale[type];
						GLES20.glUniform4f(programUniforms[UNI_VIEWPOSSCALE], view[0], view[1], view[2], view[3]);
	
						for (int j = 0; j < count; ++j) {
							
							// Set sprite uniforms
							
							GLES20.glUniform4f(programUniforms[UNI_POSSCALE], 
									spriteData[dataIndex + RenderList.ELEM_X], spriteData[dataIndex + RenderList.ELEM_Y], 
									spriteData[dataIndex + RenderList.ELEM_WIDTH], spriteData[dataIndex + RenderList.ELEM_HEIGHT]);
							
							GLES20.glUniform4f(programUniforms[UNI_UVPOSSCALE], 
									spriteData[dataIndex + RenderList.ELEM_U], spriteData[dataIndex + RenderList.ELEM_V], 
									spriteData[dataIndex + RenderList.ELEM_UVWIDTH], spriteData[dataIndex + RenderList.ELEM_UVHEIGHT]);
							
							GLES20.glUniform2f(programUniforms[UNI_ROT], 
									spriteData[dataIndex + RenderList.ELEM_DIRX], spriteData[dataIndex + RenderList.ELEM_DIRY]);
							
							GLES20.glUniform4f(programUniforms[UNI_COLOR], 
									spriteData[dataIndex + RenderList.ELEM_RCOLOR], spriteData[dataIndex + RenderList.ELEM_GCOLOR], 
									spriteData[dataIndex + RenderList.ELEM_BCOLOR], spriteData[dataIndex + RenderList.ELEM_ACOLOR]);
							
							int textureId = textures.get(Float.floatToRawIntBits(spriteData[dataIndex + RenderList.ELEM_TEXID]));
							if (boundTexture != textureId) {
								boundTexture = textureId;
								GLES20.glBindTexture(GL20.GL_TEXTURE_2D, textureId);
								//Log.d(MainActivity.TAG, "Renderer.onDrawFrame: binding texture " + textureId);
							}
							
							// Draw
							GLES20.glDrawArrays(GL20.GL_TRIANGLE_STRIP, 0, 4);
							
							dataIndex += RenderList.NUM_ELEMENTS;
						}
					}
				}
				
				GLES20.glDisableVertexAttribArray(programAttributes[ATTR_POS]);
				GLES20.glDisableVertexAttribArray(programAttributes[ATTR_UV]);
				
				GLES20.glUseProgram(0);
				GLES20.glBindBuffer(GL20.GL_ARRAY_BUFFER, 0);
				
			}
			
			int glError = GLES20.glGetError();
			if (glError != GL20.GL_NO_ERROR) {
				Log.w(MainActivity.TAG, "Renderer.onDrawFrame: glError: 0x" + Integer.toHexString(glError));
			}
			
			
			// FPS CODE
			if (System.nanoTime() - fpsStart > 1e9) {
				fpsStart = System.nanoTime();
				outIntData.add(MSG_RENDERFPS);
				outIntData.add(frames);
				frames = 0;
			}
			frames++;
			
		}
	}

	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {
		synchronized (this) {
			GLES20.glViewport(0, 0, width, height);
			
			aspectRatio = (float)width / height;
			
			viewPosScale[RenderList.SCREEN_ID][0] = -1;
			viewPosScale[RenderList.SCREEN_ID][1] = -1;
			viewPosScale[RenderList.SCREEN_ID][2] = 2 / aspectRatio;
			viewPosScale[RenderList.SCREEN_ID][3] = 2;
		}
	}

	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		synchronized (this) {
	
			if (firstCreate) {
				firstCreate = false;
			} else {
				validResources = false;
				sendContextCreateMsg = true;
				textures.clear();
			}
			
			createShaders();
			createBuffers();

			GLES20.glDisable(GL20.GL_CULL_FACE);
			GLES20.glDisable(GL20.GL_DEPTH_TEST);
			GLES20.glEnable(GL20.GL_BLEND);
			GLES20.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);

		}
	}
	
	public RenderList getRenderList() {
		return renderList;
	}
	
	public void setRenderList(RenderList renderList) {
		this.renderList = renderList;
	}
	
	private void createShaders() {
		IntBuffer errorBuf = ByteBuffer.allocateDirect(4).order(ByteOrder.nativeOrder()).asIntBuffer();
		
		programId = GLES20.glCreateProgram();
		
		vertShaderId = GLES20.glCreateShader(GL20.GL_VERTEX_SHADER);
		GLES20.glShaderSource(vertShaderId, ShaderSource.getVsSource());
		GLES20.glCompileShader(vertShaderId);
		GLES20.glGetShaderiv(vertShaderId, GL20.GL_COMPILE_STATUS, errorBuf);
		if (errorBuf.get(0) == GL20.GL_FALSE) {
			Log.e("Cave", "Renderer: glCompileShader failed");
		}
		GLES20.glAttachShader(programId, vertShaderId);
		
		
		fragShaderId = GLES20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
		GLES20.glShaderSource(fragShaderId, ShaderSource.getFsSource());
		GLES20.glCompileShader(fragShaderId);
		GLES20.glGetShaderiv(fragShaderId, GL20.GL_COMPILE_STATUS, errorBuf);
		if (errorBuf.get(0) == GL20.GL_FALSE) {
			Log.e("Cave", "Renderer: glCompileShader failed");
		}
		GLES20.glAttachShader(programId, fragShaderId);
		
		GLES20.glLinkProgram(programId);
		GLES20.glGetProgramiv(programId, GL20.GL_LINK_STATUS, errorBuf);
		if (errorBuf.get(0) == GL20.GL_FALSE) {
			Log.e("Cave", "Renderer: glLinkProgram failed");
		}
		
		String[] uniformNames = {"uViewPosScale", "uPosScale", "uUvPosScale", "uRot", "uColor", "tex0"};
		programUniforms = new int[uniformNames.length];
		for (int i = 0; i < uniformNames.length; ++i) {
			int loc = GLES20.glGetUniformLocation(programId, uniformNames[i]);
			if (loc == -1) {
				Log.e("Cave", "Renderer: uniform '" + uniformNames[i] + "' not found");
			} else {
				programUniforms[i] = loc;
			}
		}
		
		String[] attributeNames = {"aPos", "aUv"};
		programAttributes = new int[attributeNames.length];
		for (int i = 0; i < attributeNames.length; ++i) {
			int loc = GLES20.glGetAttribLocation(programId, attributeNames[i]);
			if (loc == -1) {
				Log.e("Cave", "Renderer: attribute '" + attributeNames[i] + "' not found");
			} else {
				programAttributes[i] = loc;
			}
		}
		
	}
	
	private void createBuffers() {
		float[] vertices = {0.0f, 0.0f, 0.0f, 0.0f,
							1.0f, 0.0f, 1.0f, 0.0f,
							0.0f, 1.0f, 0.0f, 1.0f,
							1.0f, 1.0f, 1.0f, 1.0f};
		
		vboId = createVbo(vertices);
		
	}
	
	private static int createVbo(Object dataArray) {
		ByteBuffer byteBuffer = null;
		int dataSize = 0;
		
		if (dataArray instanceof float[]) {
			dataSize = ((float[])dataArray).length * 4;
			
			byteBuffer = ByteBuffer.allocate(dataSize);
			byteBuffer.order(ByteOrder.nativeOrder());
			
			FloatBuffer floatBuffer = byteBuffer.asFloatBuffer();
			floatBuffer.put((float[])dataArray);
			floatBuffer.position(0);

		} else if (dataArray instanceof int[]) {
			dataSize = ((int[])dataArray).length * 4;
			
			byteBuffer = ByteBuffer.allocate(dataSize);
			byteBuffer.order(ByteOrder.nativeOrder());
			
			IntBuffer intBuffer = byteBuffer.asIntBuffer();
			intBuffer.put((int[])dataArray);
			intBuffer.position(0);
			
		} else {
			throw new RuntimeException("Renderer.createVbo: invalid data type");
		}
		
		IntBuffer vboIdBuffer = ByteBuffer.allocateDirect(4).order(ByteOrder.nativeOrder()).asIntBuffer();
		GLES20.glGenBuffers(1, vboIdBuffer);
		int vboId = vboIdBuffer.get(0);
		
		GLES20.glBindBuffer(GL20.GL_ARRAY_BUFFER, vboId);
		GLES20.glBufferData(GL20.GL_ARRAY_BUFFER, dataSize, 
				byteBuffer, GL20.GL_STATIC_DRAW);
		GLES20.glBindBuffer(GL20.GL_ARRAY_BUFFER, 0);
		
		return vboId;
	}
	
	public IntArray syncRenderMsgQueues(RenderMsgQueue msgQueue) {
		renderMsgQueue.addMessages(msgQueue);
		msgQueue.clear();
		
		if (!validResources && sendContextCreateMsg) {
			sendContextCreateMsg = false;
			outIntData.add(MSG_CONTEXTCREATED);
		}
		
		return outIntData;
	}
	
	private void processMessages(RenderMsgQueue msgQueue) {
		
		IntArray intData = msgQueue.getIntData();
		ArrayList<Object> objectData = msgQueue.getObjectData();
		int intsProcessed = 0;
		
		while (intsProcessed < intData.size()) {
			
			switch (intData.get(intsProcessed)) {
				case RenderMsgQueue.MSG_CREATETEXTURE:
				{
					int[] pixels = (int[])objectData.get(intData.get(intsProcessed + 1));
									
					IntBuffer pixelBuffer = ByteBuffer.allocateDirect(pixels.length * 4).order(ByteOrder.nativeOrder()).asIntBuffer();
					pixelBuffer.put(pixels);
					pixelBuffer.position(0);

					int width = intData.get(intsProcessed + 2);
					int height = intData.get(intsProcessed + 3);
					
					int wrapMode = intData.get(intsProcessed + 4);
					int filterMode = intData.get(intsProcessed + 5);
					
					IntBuffer idBuffer = ByteBuffer.allocateDirect(4).order(ByteOrder.nativeOrder()).asIntBuffer();
					GLES20.glGenTextures(1, idBuffer);
					int hwId = idBuffer.get(0);
					int id = textures.add(hwId);

					int glWrapMode = wrapMode == TextureManager.Atlas.WRAP_REPEAT ? GL20.GL_REPEAT : GL20.GL_CLAMP_TO_EDGE;
					int glFilterMode = filterMode == TextureManager.Atlas.FILTER_LINEAR ? GL20.GL_LINEAR : GL20.GL_NEAREST;
					
					GLES20.glBindTexture(GL20.GL_TEXTURE_2D, hwId);
					GLES20.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, glWrapMode);
					GLES20.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, glWrapMode);
					GLES20.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, glFilterMode);
					GLES20.glTexParameterf(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, glFilterMode);
					GLES20.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_RGBA, width, height, 0, 
							GL20.GL_RGBA, GL20.GL_UNSIGNED_BYTE, pixelBuffer);
					GLES20.glBindTexture(GL20.GL_TEXTURE_2D, 0);
					
					Log.i(MainActivity.TAG, "Renderer: created texture with id " + id);
					
					intsProcessed += 6;
					break;
				}
					
				case RenderMsgQueue.MSG_DESTROYTEXTURE:
				{
					
					int id = intData.get(intsProcessed + 1);

					IntBuffer idBuffer = ByteBuffer.allocateDirect(4).order(ByteOrder.nativeOrder()).asIntBuffer();
					idBuffer.put(textures.get(id));
					idBuffer.position(0);
					textures.remove(id);
					
					GLES20.glDeleteTextures(1, idBuffer);
					
					Log.i(MainActivity.TAG, "Renderer: deleted texture with id " + id);
					
					intsProcessed += 2;
					break;
				}
				
				case RenderMsgQueue.MSG_RESOURCESCREATED:
				{
					validResources = true;
					intsProcessed += 1;
					break;
				}
			}
		}
		
	}
}
